import sys
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.figure import Figure
from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, 
                            QPushButton, QLabel, QSpinBox, QFileDialog, QTableWidget, 
                            QTableWidgetItem, QHeaderView, QGroupBox, QSplitter)
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QClipboard
import pandas as pd
import locale
import platform
import matplotlib as mpl

# 配置matplotlib支持中文显示
def configure_matplotlib_fonts():
    # 设置支持中文的字体列表，按优先级排序
    font_list = ['SimHei', 'Microsoft YaHei', 'SimSun', 'Arial Unicode MS', 'DejaVu Sans']
    
    # 检查系统中的字体
    font_manager = mpl.font_manager.FontManager()
    system_fonts = set([f.name for f in font_manager.ttflist])
    
    # 找到存在于系统中的中文字体
    available_fonts = []
    for font in font_list:
        if font in system_fonts:
            available_fonts.append(font)
    
    if available_fonts:
        # 使用找到的第一个可用字体
        plt.rcParams['font.sans-serif'] = available_fonts + ['sans-serif']
    else:
        # 如果没有找到任何一个中文字体，使用系统默认字体
        plt.rcParams['font.sans-serif'] = ['sans-serif']
    
    plt.rcParams['axes.unicode_minus'] = False  # 用来正常显示负号

class MplCanvas(FigureCanvas):
    def __init__(self, parent=None, width=5, height=4, dpi=100):
        self.fig = Figure(figsize=(width, height), dpi=dpi)
        self.fig.subplots_adjust(left=0.11, right=0.95, bottom=0.13, top=0.92)
        self.axes = self.fig.add_subplot(111)
        
        # 设置字体属性
        for label in (self.axes.get_xticklabels() + self.axes.get_yticklabels()):
            label.set_fontsize(10)
            
        super(MplCanvas, self).__init__(self.fig)

class PolynomialFitterApp(QMainWindow):
    def __init__(self):
        super().__init__()
        
        # 数据存储
        self.x_data = None
        self.y_data = None
        self.poly_coeffs = None
        self.poly_func = None
        self.degree = 2  # 默认多项式次数
        
        self.setWindowTitle("多项式拟合工具")
        self.setGeometry(100, 100, 1200, 800)
        
        # 设置应用程序字体
        app_font = self.font()
        app_font.setFamily("Microsoft YaHei")
        app_font.setPointSize(10)
        self.setFont(app_font)
        
        # 创建主布局
        main_widget = QWidget()
        main_layout = QVBoxLayout(main_widget)
        
        # 创建上下分割区域
        splitter = QSplitter(Qt.Vertical)
        
        # 上部分区域：包含控制面板和数据表格
        top_widget = QWidget()
        top_layout = QHBoxLayout(top_widget)
        
        # 控制面板
        control_group = QGroupBox("控制面板")
        control_layout = QVBoxLayout()
        
        # 导入数据按钮
        self.import_btn = QPushButton("导入数据")
        self.import_btn.clicked.connect(self.import_data)
        control_layout.addWidget(self.import_btn)
        
        # 多项式次数选择
        degree_layout = QHBoxLayout()
        degree_layout.addWidget(QLabel("多项式次数:"))
        self.degree_spin = QSpinBox()
        self.degree_spin.setRange(1, 10)
        self.degree_spin.setValue(self.degree)
        self.degree_spin.valueChanged.connect(self.update_degree)
        degree_layout.addWidget(self.degree_spin)
        control_layout.addLayout(degree_layout)
        
        # 拟合按钮
        self.fit_btn = QPushButton("拟合多项式")
        self.fit_btn.clicked.connect(self.fit_polynomial)
        self.fit_btn.setEnabled(False)
        control_layout.addWidget(self.fit_btn)
        
        # 智能拟合按钮
        self.auto_fit_btn = QPushButton("智能拟合多项式")
        self.auto_fit_btn.clicked.connect(self.auto_fit_polynomial)
        self.auto_fit_btn.setEnabled(False)
        control_layout.addWidget(self.auto_fit_btn)
        
        # 公式显示
        formula_label_title = QLabel("拟合公式:")
        control_layout.addWidget(formula_label_title)
        
        # 公式显示和复制按钮布局
        formula_layout = QHBoxLayout()
        self.formula_label = QLabel("尚未拟合")
        self.formula_label.setWordWrap(True)
        self.formula_label.setMinimumHeight(40)
        self.formula_label.setStyleSheet("font-family: 'Microsoft YaHei'; padding: 5px; background-color: #f0f0f0; border-radius: 3px;")
        formula_layout.addWidget(self.formula_label)
        
        # 复制公式按钮
        self.copy_formula_btn = QPushButton("复制公式")
        self.copy_formula_btn.clicked.connect(self.copy_formula_to_clipboard)
        self.copy_formula_btn.setEnabled(False)
        formula_layout.addWidget(self.copy_formula_btn)
        
        control_layout.addLayout(formula_layout)
        
        # 统计信息
        stats_label_title = QLabel("拟合统计:")
        control_layout.addWidget(stats_label_title)
        self.stats_label = QLabel("尚未拟合")
        self.stats_label.setWordWrap(True)
        self.stats_label.setStyleSheet("font-family: 'Microsoft YaHei'; padding: 5px; background-color: #f0f0f0; border-radius: 3px;")
        control_layout.addWidget(self.stats_label)
        
        control_layout.addStretch()
        control_group.setLayout(control_layout)
        
        # 数据表格
        table_group = QGroupBox("数据表")
        table_layout = QVBoxLayout()
        self.data_table = QTableWidget(0, 3)
        self.data_table.setHorizontalHeaderLabels(["X", "Y实际", "Y预测"])
        self.data_table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
        table_layout.addWidget(self.data_table)
        table_group.setLayout(table_layout)
        
        # 添加控制面板和数据表格到上部分布局
        top_layout.addWidget(control_group, 1)
        top_layout.addWidget(table_group, 2)
        
        # 下部分区域：可视化图表
        self.canvas = MplCanvas(self, width=5, height=4, dpi=100)
        
        # 将区域添加到分割器
        splitter.addWidget(top_widget)
        splitter.addWidget(self.canvas)
        
        # 设置分割区域的初始大小
        splitter.setSizes([400, 400])
        
        # 添加分割区域到主布局
        main_layout.addWidget(splitter)
        
        self.setCentralWidget(main_widget)
    
    def import_data(self):
        options = QFileDialog.Options()
        file_name, _ = QFileDialog.getOpenFileName(
            self, "选择数据文件", "", 
            "Excel文件 (*.xlsx *.xls);;CSV文件 (*.csv);;所有文件 (*)", 
            options=options
        )
        
        if file_name:
            try:
                if file_name.endswith(('.xlsx', '.xls')):
                    data = pd.read_excel(file_name)
                elif file_name.endswith('.csv'):
                    data = pd.read_csv(file_name)
                else:
                    raise ValueError("不支持的文件格式")
                
                # 检查文件至少有两列数据
                if data.shape[1] < 2:
                    raise ValueError("数据文件必须至少包含两列数据 (X和Y)")
                
                # 提取前两列作为X和Y数据
                self.x_data = data.iloc[:, 0].values
                self.y_data = data.iloc[:, 1].values
                
                # 更新表格
                self.update_table()
                
                # 绘制原始数据散点图
                self.plot_data()
                
                # 允许拟合
                self.fit_btn.setEnabled(True)
                self.auto_fit_btn.setEnabled(True)
                
            except Exception as e:
                self.formula_label.setText(f"导入错误: {str(e)}")
    
    def update_table(self):
        if self.x_data is None or self.y_data is None:
            return
        
        # 清空表格
        self.data_table.setRowCount(0)
        
        # 设置行数
        n_rows = len(self.x_data)
        self.data_table.setRowCount(n_rows)
        
        # 填充X和Y实际值
        for i in range(n_rows):
            x_item = QTableWidgetItem(f"{self.x_data[i]:.6g}")
            y_item = QTableWidgetItem(f"{self.y_data[i]:.6g}")
            
            self.data_table.setItem(i, 0, x_item)
            self.data_table.setItem(i, 1, y_item)
            
            # 如果已拟合，添加预测值
            if self.poly_func is not None:
                y_pred = self.poly_func(self.x_data[i])
                y_pred_item = QTableWidgetItem(f"{y_pred:.6g}")
                self.data_table.setItem(i, 2, y_pred_item)
    
    def update_degree(self, value):
        self.degree = value
    
    def fit_polynomial(self):
        if self.x_data is None or self.y_data is None:
            return
        
        try:
            # 拟合多项式
            self.poly_coeffs = np.polyfit(self.x_data, self.y_data, self.degree)
            self.poly_func = np.poly1d(self.poly_coeffs)
            
            # 计算统计指标
            y_pred = self.poly_func(self.x_data)
            mse = np.mean((self.y_data - y_pred) ** 2)
            rmse = np.sqrt(mse)
            
            # 计算R²
            y_mean = np.mean(self.y_data)
            ss_total = np.sum((self.y_data - y_mean) ** 2)
            ss_residual = np.sum((self.y_data - y_pred) ** 2)
            r2 = 1 - (ss_residual / ss_total)
            
            # 更新公式显示
            formula = "y = "
            for i, coef in enumerate(self.poly_coeffs):
                power = self.degree - i
                if i > 0:
                    formula += " + " if coef >= 0 else " - "
                    formula += f"{abs(coef):.6g}"
                else:
                    formula += f"{coef:.6g}"
                
                if power > 0:
                    formula += f"x"
                    if power > 1:
                        formula += f"^{power}"
            
            self.formula_label.setText(formula)
            
            # 启用复制公式按钮
            self.copy_formula_btn.setEnabled(True)
            
            # 更新统计信息
            self.stats_label.setText(f"MSE: {mse:.6g}, RMSE: {rmse:.6g}, R²: {r2:.6g}")
            
            # 更新表格
            self.update_table()
            
            # 更新图
            self.plot_fitted_curve()
            
        except Exception as e:
            self.formula_label.setText(f"拟合错误: {str(e)}")
            self.copy_formula_btn.setEnabled(False)
    
    def plot_data(self):
        self.canvas.axes.clear()
        if self.x_data is not None and self.y_data is not None:
            self.canvas.axes.scatter(self.x_data, self.y_data, color='blue', label='原始数据')
            self.canvas.axes.set_xlabel('X', fontsize=12)
            self.canvas.axes.set_ylabel('Y', fontsize=12)
            self.canvas.axes.set_title('数据散点图', fontsize=14)
            self.canvas.axes.grid(True)
            self.canvas.axes.legend(fontsize=10)
            self.canvas.fig.tight_layout()
            self.canvas.draw()
    
    def plot_fitted_curve(self):
        self.canvas.axes.clear()
        if self.x_data is not None and self.y_data is not None:
            # 绘制原始数据点
            self.canvas.axes.scatter(self.x_data, self.y_data, color='blue', label='原始数据')
            
            if self.poly_func is not None:
                # 创建更密集的x点以绘制平滑曲线
                x_line = np.linspace(min(self.x_data), max(self.x_data), 1000)
                y_line = self.poly_func(x_line)
                
                # 绘制拟合曲线
                self.canvas.axes.plot(x_line, y_line, 'r-', label='拟合曲线')
                
                # 计算并显示预测值
                y_pred = self.poly_func(self.x_data)
                
                # 绘制预测点
                self.canvas.axes.scatter(self.x_data, y_pred, color='green', alpha=0.5, label='预测值')
                
                # 绘制从实际到预测的线
                for i in range(len(self.x_data)):
                    self.canvas.axes.plot([self.x_data[i], self.x_data[i]], 
                                         [self.y_data[i], y_pred[i]], 
                                         'gray', linestyle='--', alpha=0.3)
            
            self.canvas.axes.set_xlabel('X', fontsize=12)
            self.canvas.axes.set_ylabel('Y', fontsize=12)
            self.canvas.axes.set_title('数据拟合', fontsize=14)
            self.canvas.axes.grid(True)
            self.canvas.axes.legend(fontsize=10)
            self.canvas.fig.tight_layout()
            self.canvas.draw()

    def auto_fit_polynomial(self):
        """智能选择最佳多项式次数进行拟合"""
        if self.x_data is None or self.y_data is None:
            return
        
        try:
            # 数据点数量
            n_points = len(self.x_data)
            
            # 最大考虑次数 (避免过拟合，不超过数据点数量的一半或10)
            max_degree = min(10, max(1, n_points // 2 - 1))
            
            best_degree = 1
            best_r2 = 0
            best_aic = float('inf')  # Akaike信息准则，越小越好
            
            # 尝试不同的多项式次数，找到最佳拟合
            for degree in range(1, max_degree + 1):
                # 拟合当前次数的多项式
                coeffs = np.polyfit(self.x_data, self.y_data, degree)
                poly = np.poly1d(coeffs)
                
                # 预测值
                y_pred = poly(self.x_data)
                
                # 计算均方误差
                mse = np.mean((self.y_data - y_pred) ** 2)
                
                # 计算R²
                y_mean = np.mean(self.y_data)
                ss_total = np.sum((self.y_data - y_mean) ** 2)
                ss_residual = np.sum((self.y_data - y_pred) ** 2)
                r2 = 1 - (ss_residual / ss_total)
                
                # 计算AIC (Akaike信息准则)
                # k是参数数量，包括常数项，即degree+1
                k = degree + 1
                aic = n_points * np.log(ss_residual / n_points) + 2 * k
                
                # 更新最佳度数
                # 使用加权方法: 高R²值好，但也要防止过拟合
                if r2 > best_r2 * 1.01:  # 如果R²显著提高
                    best_degree = degree
                    best_r2 = r2
                    best_aic = aic
                elif r2 > best_r2 * 0.99 and aic < best_aic * 0.95:  # R²相似但AIC显著降低
                    best_degree = degree
                    best_r2 = r2
                    best_aic = aic
            
            # 设置最佳度数并执行拟合
            self.degree_spin.setValue(best_degree)
            self.degree = best_degree
            self.fit_polynomial()
            
            # 更新信息，告知用户选择的最佳次数
            current_stats = self.stats_label.text()
            self.stats_label.setText(f"{current_stats}, 智能选择次数: {best_degree}")
            
        except Exception as e:
            self.formula_label.setText(f"智能拟合错误: {str(e)}")

    def copy_formula_to_clipboard(self):
        """复制公式到剪贴板"""
        if self.formula_label.text() != "尚未拟合" and not self.formula_label.text().startswith("拟合错误"):
            clipboard = QApplication.clipboard()
            clipboard.setText(self.formula_label.text())
            # 可以添加一个临时提示，但不是必需的
            original_text = self.copy_formula_btn.text()
            self.copy_formula_btn.setText("已复制!")
            # 使用计时器在1秒后恢复按钮文本
            QApplication.processEvents()
            import time
            time.sleep(0.5)
            self.copy_formula_btn.setText(original_text)

def main():
    # 配置matplotlib字体
    configure_matplotlib_fonts()
    
    # 根据操作系统类型设置中文显示
    system = platform.system()
    try:
        if system == 'Windows':
            locale.setlocale(locale.LC_ALL, 'chinese')
        elif system == 'Linux':
            locale.setlocale(locale.LC_ALL, 'zh_CN.UTF-8')
        elif system == 'Darwin':  # macOS
            locale.setlocale(locale.LC_ALL, 'zh_CN.UTF-8')
    except:
        pass  # 如果设置失败，使用默认值
    
    app = QApplication(sys.argv)
    window = PolynomialFitterApp()
    window.show()
    sys.exit(app.exec_())

if __name__ == "__main__":
    main() 